Parte 8 - Introdução a Planos

Contexto

Aqui nós introduzimos um objeto que é crucial para escalar a indústria do Aprendizado Federado: Planos. Isso reduz dramaticamente o uso de largura de banda, permite esquemas assíncronos e da mais autonomia para dispositivos remotos. O conceito original do plano pode ser encontrado no artigo Towards Federated Learning at Scale: System Design, porém com algumas adaptações para as nossas necessidades no PySyft.

Um plano tem a intenção de salvar uma sequência de operações do torch , como uma função, mas permitindo enviar essa sequência de operações para um worker remoto e manter a referência dele. Dessa forma, para computar remotamente essa sequência de $n$ operações em alguma entrada referenciada através desses ponteiros, ao invés de mandar $n$ mensagens, você precisa agora enviar apenas uma única mensagem com as referências para o plano e para os ponteiros. Você pode também fornecer tensores com sua função (que chamamos state tensors ) para ter as funcionalidades extendidas. Planos podem ser vistos tanto como uma função que você pode enviar, como uma classe que também pode ser enviada e executada remotamente. Portanto, para usuários de alto nível, a noção de plano desaparece e é substituída por um recurso mágico que permite enviar para workers remotos funções arbitrárias que contém uma sequência de funções torch .

Uma coisa a se notar é que a classe de funções que você quer transformar em planos é atualmente limitada para sequências de operações hook (gancho) do torch , exclusivamente. Em particular, isso exclui estruturas lógicas como if, for e while, mesmo que estejamos trabalhando para ter soluções em breve. Para ser completamente preciso, você pode usá-los, mas o caminho lógico que você toma (primeiro if é falso e 5 laços for, por exemplo) na primeira computação do seu plano será levado por todas as próximas computações, o qual queremos, na maioria dos casos, evitar.

Autores:

Tradutor:

Importações e especificações do modelo

Primeiro faremos as importações oficiais.


In [ ]:
import torch
import torch.nn as nn
import torch.nn.functional as F

E então aqueles específicos do PySyft, com uma importante observação: o worker local não deve ser um worker cliente. Workers não clientes podem salvar objetos e precisamos dessa funcionalidade para usar um plano.


In [ ]:
import syft as sy  # importe a biblioteca PySyft
hook = sy.TorchHook(torch)  # hook PyTorch i.e. adiciona funcionalidades extras 

# IMPORTANTE: worker local não deve ser um worker cliente
hook.local_worker.is_client_worker = False


server = hook.local_worker

Para ser consistente com o conceito fornecido no artigo referenciado, definimos workers remotos ou devices, provendo eles com algum dado.


In [ ]:
x11 = torch.tensor([-1, 2.]).tag('input_data')
x12 = torch.tensor([1, -2.]).tag('input_data2')
x21 = torch.tensor([-1, 2.]).tag('input_data')
x22 = torch.tensor([1, -2.]).tag('input_data2')

device_1 = sy.VirtualWorker(hook, id="device_1", data=(x11, x12)) 
device_2 = sy.VirtualWorker(hook, id="device_2", data=(x21, x22))
devices = device_1, device_2

Exemplo Básico

Vamos definir uma função que queremos transformar em um plano. Para isso, apenas adiciona-se um decorator (decorador) acima da definição da função.


In [ ]:
@sy.func2plan()
def plan_double_abs(x):
    x = x + x
    x = torch.abs(x)
    return x

Vamos verificar. Sim, agora nós temos um plano!


In [ ]:
plan_double_abs

Para usar um plano, precisamos de duas coisas: contruir um plano (i.e. registrar uma sequência de operações presentes na função) e mandar isso para um worker / device. Felizmente você pode fazer isso de uma maneira muito fácil.

Construindo um plano

Para construir um plano, você só precisa chamar isso em algum dado.

Vamos primeiro pegar a referência para algum dado remoto: uma requisição é enviada para a rede e o ponteiro de referência é retornado.


In [ ]:
pointer_to_data = device_1.search('input_data')[0]
pointer_to_data

Se dissermos para um plano que ele deve ser executado remotamente no dispositivo location:device_1, isso retornará um erro pois o plano não foi construído ainda.


In [ ]:
plan_double_abs.is_built

In [ ]:
# Falha acontece se mandar um plano não construído
try:
    plan_double_abs.send(device_1)
except RuntimeError as error:
    print(error)

Para construir um plano só precisamos chamar build no plano e passar os argumentos necessários para a execução de um plano (a.k.a algum dado). Quando um plano é construído, todos os comandos são executados sequencialmente por um worker local, capturados pelo plano e então salvos no atributo actions!


In [ ]:
plan_double_abs.build(torch.tensor([1., -2.]))

In [ ]:
plan_double_abs.is_built

Agora, se tentarmos enviar um plano, irá funcionar!


In [ ]:
# Essa célula é executada com sucesso
pointer_plan = plan_double_abs.send(device_1)
pointer_plan

Como nos tensores, podemos ver um ponteiro para o objeto que foi enviado, sendo chamado simplesmente de PointerPlan.

Um lembrete importante é que, quando um plano é construído, nós de antemão pré-definimos os id(s) dos resultados das computações que devem ser salvos. Isso irá permitir enviar os comandos de forma assíncrona, já ter uma referência para um resultado virtual e continuar as computações locais sem ter que esperar os resultados remotos serem computados. Uma aplicação importante é quando você exige a computação de um grupo no device_1 e não quer esperar pelo fim dessa computação para mandar outro grupo de computação para o device_2

Executando um Plano Remotamente

Nós agora podemos executar um plano remotamente chamando um ponteiro para um plano com um ponteiro para algum dado. Isso emite um comando para executar esse plano remotamente, sendo assim, a localização pré-definida da saída do plano agora contém o resultado (lembre-se que pré-definimos a localização do resultado antes da computação). Isso também requer uma única rodada de comunicação.

O resultado é simplesmente um ponteiro, assim como quando você chama uma função do gancho no torch .


In [ ]:
pointer_to_result = pointer_plan(pointer_to_data)
print(pointer_to_result)

E você pode simplesmentes pegar o valor de volta.


In [ ]:
pointer_to_result.get()

Rumo a um exemplo concreto

Mas nós queremos que Planos sejam aplicados para aprendizado profundo e federado, certo? Então vamos dar uma olhada em um exemplo ligeiramente mais complicado, usando rede neural que certamente você deve estar ansioso para usar. Note que estamos agora transformando uma classe em um Plano. Para isso, nós herdamos a nossa classe de sy.Plan (ao invés de herdar de nn.Module)


In [ ]:
class Net(sy.Plan):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(2, 3)
        self.fc2 = nn.Linear(3, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

In [ ]:
net = Net()

In [ ]:
net

Vamos construir um plano usando dados de exemplo.


In [ ]:
net.build(torch.tensor([1., 2.]))

Agora enviamos o Plano para um worker remoto.


In [ ]:
pointer_to_net = net.send(device_1)
pointer_to_net

Vamos recuperar alguns dados remotos.


In [ ]:
pointer_to_data = device_1.search('input_data')[0]

A sintaxe é como a execução sequencial remota, isso é, como uma execução local. Mas comparada a uma execução remota clássica, tem apenas uma rodada de comunicação para cada execução.


In [ ]:
pointer_to_result = pointer_to_net(pointer_to_data)
pointer_to_result

E recebemos o resultado como sempre!


In [ ]:
pointer_to_result.get()

Et voilà! Nós vimos como dramaticamente reduzir a comunicação entre worker local (ou servidor) e os dispositivos remotos.

Troca entre Workers

Uma característica importante que queremos ter é usar o mesmo plano para vários workers, que podemos mudar dependendo da parte remota do dado que estamos considerando. Em especial, não queremos reconstruir o plano cada vez que temos que mudar de worker. Vamos ver como fazer isso usando o exemplo visto anteriormente com a nossa pequena rede.


In [ ]:
class Net(sy.Plan):
    def __init__(self):
        super(Net, self).__init__()
        self.fc1 = nn.Linear(2, 3)
        self.fc2 = nn.Linear(3, 2)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = self.fc2(x)
        return F.log_softmax(x, dim=0)

In [ ]:
net = Net()

# Build plan
net.build(torch.tensor([1., 2.]))

Aqui são executados os passos principais


In [ ]:
pointer_to_net_1 = net.send(device_1)
pointer_to_data = device_1.search('input_data')[0]
pointer_to_result = pointer_to_net_1(pointer_to_data)
pointer_to_result.get()

E você pode construir outro PointerPlan a partir do mesmo plano, sendo a sintaxe a mesma para executar o plano remotamente em outro dispositivo.


In [ ]:
pointer_to_net_2 = net.send(device_2)
pointer_to_data = device_2.search('input_data')[0]
pointer_to_result = pointer_to_net_2(pointer_to_data)
pointer_to_result.get()

Note: Atualmente, com a classe Plan, você pode apenas usar um único método e tem que nomear como "forward"

Construindo planos automaticamente que são funções

Para funções (@ sy.func2plan) nós podemos automaticamente construir um plano sem e necessidada de explicitar a chamada do build, uma vez que no momento da criação, o plano já é criado.

Para usar essa funcionalidade, a única coisa que você deve mudar na criação do plano é definir um argumento para o decorator chamado args_shape que deve ser uma lista que contém os shapes de cada argumento.


In [ ]:
@sy.func2plan(args_shape=[(-1, 1)])
def plan_double_abs(x):
    x = x + x
    x = torch.abs(x)
    return x

plan_double_abs.is_built

O parâmetro args_shape é usado internamente para criar tensores de exemplo com o formato no qual foi passado para construir o plano.


In [ ]:
@sy.func2plan(args_shape=[(1, 2), (-1, 2)])
def plan_sum_abs(x, y):
    s = x + y
    return torch.abs(s)

plan_sum_abs.is_built

Também é possível prover estado de elementos para funções!


In [ ]:
@sy.func2plan(args_shape=[(1,)], state=(torch.tensor([1]), ))
def plan_abs(x, state):
    bias, = state.read()
    x = x.abs()
    return x + bias

In [ ]:
pointer_plan = plan_abs.send(device_1)
x_ptr = torch.tensor([-1, 0]).send(device_1)
p = pointer_plan(x_ptr)
p.get()

Para aprender mais sobre isso, você pode descobrir como usar Planos com Protocolos no tutorial Parte 8 bis!

Dê-nos uma estrela em nosso repo do PySyft no GitHub

A maneira mais fácil de ajudar nossa comunidade é adicionando uma estrela nos nossos repositórios! Isso ajuda a aumentar a conscientização sobre essas ferramentas legais que estamos construindo.

Veja nossos tutoriais no GitHub!

Fizemos tutoriais muito bons para entender melhor como deve ser a Aprendizagem Federada e a proteção de Privacidade, e como estamos construindo as coisas básicas que precisamos para fazer com que isso aconteça.

Junte-se ao Slack!

A melhor maneira de manter-se atualizado sobre os últimos avanços é se juntar à nossa comunidade!

Contribua com o projeto!

A melhor maneira de contribuir para a nossa comunidade é se tornando um contribuidor do código! A qualquer momento, você pode acessar a página de Issues (problemas) do PySyft no GitHub e filtrar por "Projetos". Isso mostrará todas as etiquetas (tags) na parte superior, com uma visão geral de quais projetos você pode participar! Se você não deseja ingressar em um projeto, mas gostaria de codificar um pouco, também pode procurar mais mini-projetos "independentes" pesquisando problemas no GitHub marcados como "good first issue".

Doar

Se você não tem tempo para contribuir com nossa base de códigos, mas ainda deseja nos apoiar, também pode se tornar um Apoiador em nosso Open Collective. Todas as doações vão para hospedagem na web e outras despesas da comunidade, como hackathons e meetups!

Página do Open Collective do OpenMined


In [ ]: